package org.owasp.esapi.reference.crypto;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Properties;

import org.owasp.esapi.EncryptedProperties;

/**
 * Command line utilities for reading, writing and creating encrypted properties files.
 * <p>
 * Usage:<br/>
 * <code>
 *    java org.owasp.esapi.reference.crypto.EncryptedPropertiesUtils [--in file] [--out file]
 *            [--in-encrypted true|false] [--verbose true|false]
 * </code>
 * <p>
 * Command line parameters:<br/>
 * <ul>
 * <li><b>--in</b> (Optional) Encrypted or plaintext file to read from. If no input file is specified, a new properties file will be created.</li>
 * <li><b>--out</b> (Optional) Encrypted file to output to. Default: Overwrite input file</li>
 * <li><b>--in-encrypted</b> (Optional) True if the input file is encrypted. Default: true</li>
 * <li><b>--verbose</b> (Optional) If true, output (potentially unencrypted) information to the terminal. Default: false</li>
 * </ul>
 *
 * @author August Detlefsen (augustd at codemagi dot com)
 *         <a href="http://www.codemagi.com">CodeMagi, Inc.</a>
 * @since October 8, 2010
 * @see org.owasp.esapi.EncryptedProperties
 */
public class EncryptedPropertiesUtils {

    /**
     * Loads encrypted or plaintext properties file based on the location passed in args
     * then prompts the user to input key-value pairs.  When the user enters a null or
     * blank key, the values are stored to the properties file.
     *
     * @throws Exception Any exception thrown
     */
    public static void main(String[] args) throws Exception {

        //command line options
        String inFile = null;
        String outFile = null;
        boolean inFileEncrypted = true;
        boolean verbose = false;

        //parse command line params
        for (int i = 0; i < args.length; i = i + 2) {
            String paramType = args[i];

            if ("--in".equals(paramType) && args.length >= i + 1) {
                inFile = args[i + 1];

            } else if ("--out".equals(paramType) && args.length >= i + 1) {
                outFile = args[i + 1];

            } else if ("--in-encrypted".equals(paramType) && args.length >= i + 1) {
                inFileEncrypted = Boolean.valueOf(args[i + 1]);

            } else if ("--verbose".equals(paramType) && args.length >= i + 1) {
                verbose = Boolean.valueOf(args[i + 1]);
            }

        }

        if (outFile == null) {
            outFile = inFile; //if no output file is specified we will overwrite the input file
        }
        if (outFile == null) {
            //no input or output file specified. Can't continue.
            System.out.println("You must specify an input file or output file");
            System.exit(1);
        }

        //load in existing properties from a file
        Properties props = loadProperties(inFile, inFileEncrypted);

        //read user input and add keys and values to the property file
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String key = null;
        do {
            System.out.print("Enter key: ");
            key = br.readLine();

            if (props.containsKey(key)) {
                System.out.print("Key already exists. Replace? ");
                String confirm = br.readLine();
                if (!("y".equals(confirm) || "yes".equals(confirm))) {
                    continue;
                }
            }

            System.out.print("Enter value: ");
            String value = br.readLine();

            addProperty(props, key, value);

        } while (key != null && key.length() > 0);

        //save output file
        storeProperties(outFile, props,
                "Encrypted Properties File generated by org.owasp.esapi.reference.crypto.EncryptedPropertiesUtils");

        System.out.println("Encrypted Properties file output to " + outFile);

        if (verbose) {
            for (Object oKey : props.keySet()) {
                String sKey = (String) oKey;
                String value = props.getProperty(sKey);
                System.out.println("   " + sKey + "=" + value);
            }
        }

    }

    /**
     * Loads a Properties file from a filename. If the filename is unspecified
     * or the file could not be found, a new Properties is returned.
     *
     * @param inFile Filename to load Properties from.
     * @param inFileEncrypted If true, the input file is assumed to be already encrypted. Default true.
     * @return Either the loaded Properties object or a new one if the file could not be found.
     * @throws IOException
     */
    public static Properties loadProperties(String inFile, Boolean inFileEncrypted) throws IOException {

        if (inFileEncrypted == null) inFileEncrypted = true;

        Properties props;

        if (inFile != null) {

            File f = new File(inFile);
            if (!f.exists()) {
                System.out.println("Input properties file not found. Creating new.");
                props = new ReferenceEncryptedProperties();

            } else {
                String encrypted = inFileEncrypted ? "Encrypted" : "Plaintext";
                System.out.println(encrypted + " properties found in " + f.getAbsolutePath());

                Properties inProperties;
                if (inFileEncrypted) {
                    inProperties = new ReferenceEncryptedProperties();
                } else {
                    inProperties = new Properties();
                }

                InputStream in = null;
                try {
                    in = new FileInputStream(f);
                    inProperties.load(in);
                } finally {
                    try {
                        if (in != null) in.close(); //quietly close the InputStream
                    } catch (Exception e) {}
                }

                //Use the existing properties
                props = new ReferenceEncryptedProperties(inProperties);
            }
        } else {
            System.out.println("Input properties file not found. Creating new.");
            props = new ReferenceEncryptedProperties();
        }

        return props;
    }

    /**
     * Stores a Properties object to a file.
     *
     * @param outFile Filename to store to
     * @param props Properties to store
     * @param message A message to add to the comments in the stored file
     * @throws Exception
     */
    public static void storeProperties(String outFile, Properties props, String message) throws Exception {
        OutputStream out = null;
        try {
            out = new FileOutputStream(new File(outFile));
            props.store(out, message);
        } finally {
            try {
                if (out != null) out.close();  //quietly close OutputStream
            } catch (Exception e) {}
        }
    }

    /**
     * Adds a new key-value property to the passed Properties object
     *
     * @param props The Properties object to add to
     * @param key The key to add
     * @param value The value to set
     * @return The previous value of the property, or null if it is newly added.
     */
    public static Object addProperty(Properties props, String key, String value) {
        if (props != null && key != null && key.length() > 0 && value != null && value.length() > 0) {
            return props.setProperty(key, value);
        }
        return null;
    }

}
